查看原文
其他

系统编程-文件读写这件小事

编程珠玑 2022-09-10
来源:公众号【编程珠玑】
作者:守望先生
ID:shouwangxiansheng

在《系统编程-文件IO》中简单介绍了文件I/O的基本流程,无论选项或者参数多么变化多端,其流程大抵相同,不过是获取文件描述符,用描述符进行操作,关闭描述符,三步而已。那么文件读写又是怎样的流程?需要注意什么?

write/read

在说明这些常见出错之前,就必须先了解其基本用法了。需要注意的是,write/read是不带缓冲的,调用一次,写一次。与fwrite/fread有区别,另外write/read为系统调用,频繁地系统调用将会增加开销,可参考《库函数和系统调用的区别》。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
参数解释:
  • fd  文件描述符,这个应该不用多做解释
  • buf 要写入的内容,或者读出内容存储的buf,合适的大小非常关键
  • count 读或写的内容大小
这里有两点需要注意一下。
返回值为ssize_t类型,因为它的返回值可以为负,表示出错,有趣的是这样一来使得其能表示的读写字节范围少了近一半
返回大于0,表示读或写入对应的字节数。对于read,返回0表示到文件结尾。
另外,我们还注意到,write函数的第二个参数由const修饰。为什么要使用const来修饰?
很显然,在写的过程中,write函数不应该对buf的内容进行修改,它仅仅是从buf中读取罢了。这里在编码时常用的设计,如果不希望该函数修改其内容,则加上const限定符。const详细说明参考《const关键字到底该怎么用?》。
那么返回的读写大小,和参数里的count大小有何区别?前者是真实读写的字节数,而后者是期望读写的字节数。举个简单的例子,文件中有16字节内容,而你尝试读64字节,自然最终只会读到16字节。

正常读写

正常读写的例子如下:
//来源:公众号【编程珠玑】
//博客:https://www.yanbinghu.com
//file.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{

    char writeBuf[] = "https://www.yanbinghu.com";
    char readBuf[128] = {0};
    /*可读可写,不存在时创建,有内容时截断*/
    int fd = open("test.txt",O_RDWR | O_CREAT | O_TRUNC);
    if(-1 == fd)
    {
        perror("open failed");
        return -1;
    }
    /*写内容*/
    ssize_t wLen = write(fd,writeBuf,sizeof(writeBuf));
    if(wLen < 0)
    {
        perror("write failed");
        close(fd);
        return -1;
    }
    printf("write len:%ld\n",wLen);
    ssize_t rLen = read(fd,readBuf,sizeof(readBuf));
    if(rLen < 0)
    {
        perror("read failed");
        close(fd);
        return -1;
    }
    readBuf[sizeof(readBuf)-1] = 0;
    printf("read content:%s\n",readBuf);
    close(fd);
    return 0;
}
编译运行,然后你就会惊喜地发现,结果并不是如你想地那样:
$ gcc -o writeFile file.c
$ ./writeFile
write len:26
read content:
我们查看文件可以看到内容可能已经写进去了,但是读取出来地内容却是空
这是为何?
理解这个问题需要理解文件描述符和偏移量。

文件描述符

文件描述符虽然只是一个整型值,但它只是一个索引值,它指向了该进程打开文件的记录表。还记得常说的“一切皆文件”吗?实际上,即使你打开一个TCP链接,都会有一个对应的文件描述符。这个记录表中包含了很多与文件相关地信息,例如文件偏移量,inode,状态标志等等。
而你每一次进行读写,都会影响所谓地文件偏移量。
因此你在第一次进行写之后,文件偏移量类似于下面这样:

那么你进行第一次读的时候,文件偏移已经到文件的末尾了(此时函数返回值为0),所以你肯定读不出任何内容,因此你需要移动偏移指针。

设置偏移量

为了读取写入后的内容,我们必须要设置偏移量,设置成像下面这样:

有人可能会好奇,这最后为什么还有一个\0?很显然,它被自动加上了,具体原因可以参考《NULL,0,'0'你真的分清了吗》。
还有人会问,你怎么看出有一个\0?用od命令看一下就知道了。
$ od -c test.txt
0000000   h   t   t   p   s   :   /   /   w   w   w   .   y   a   n   b
0000020   i   n   g   h   u   .   c   o   m  \0
0000032
现在看到了吧。
为了设置偏移量,我们需要用到函数lseek:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
成功返回新的文件偏移量,出错返回-1。
有必要对参数进行解释
  • offset 相对于whence的偏移量
  • whence 相对位置
其中whence有三个值
  • SEEK_SET  文件开始处
  • SEEK_CUR  当前位置
  • SEEK_END   文件末尾
举个例子,假设当前offset为-4,whence为SEEK_CUR,那么当写完内容,并设置该选项后的文件偏移位置如下:


注意,offset是可以为负的。
说白了可以设置偏移位置,而设置可以相对三个位置,开头,当前和结尾。

读取写入的内容

好了,为了读取到我们写入的内容,我们已经知道怎么做了,就是设置偏移量在文件开头,即在读之前加上下面的语句:
lseek(fd, 0, SEEK_SET);//注意检查返回值
然后再次编译运行:
write len:26
read content:https://www.yanbinghu.com
如你所愿!

常见报错

使用不当或者出错的时候会有错误信息,这在编码的时候就需要注意检查。

Bad file descriptor

通常使用了一个并不合法的文件描述符,例如,该文件描述符已经关闭。通常你可以通过下面的命令来观察文件描述符的打开情况:
$ ls -al /proc/`pidof procName`/fd/
这里的procName是你正在运行的程序名。
也有可能是你打开模式不对,例如,以只读方式打开,却尝试写。

Interrupted system call

通常是在读写过程中被中断,常见的如对socket进行读写时,链接被意外中断,或者读写时,进程被中断等等。

File exists

通常在你想创建一个文件,但是文件已经存在的情况。

No such file or directory

就如字面意思,通常是文件或者目录不存在,也许你使用了O_CREATE标志,但是如果你的目录不存在,文件也无法创建成功。
还有一种情况是,你已经打开了该文件,程序执行过程中,该文件又被人删除了,删除后又创建了一个文件名一样的文件,这样的情况下,也有可能会提示该错误。

Too many open fileswrite

进程打开的文件过多。一个进程打开的文件数量是有限的,具体可以通过:
$ ulimit -n
65535
至于当前已经打开了多少,可以这样统计:
$ ls -l /proc/`pidof proName`/fd/ |wc -l
proName为你的进程名。

总结

一些常见错误中很多涉及到网络的读写,这里暂时没有提及。
一般情况,不会用同一个文件描述符对文件进行既读又写,一旦出现这样的场景时,需要注意偏移量的设置。虽然本文的I/O函数不带缓冲,但是读写时,选择合适的buf大小也非常关键。
另外编程中也有以下建议:
  • 检查接口的返回值,处理出错场景
  • 对于不期望被修改内容的参数,添加const限定符
  • 善用man手册


相关精彩推荐

那些奇奇怪怪的缓冲问题
系统编程-简洁而不简单的文件操作
NULL,0,'\0',“0”,"\0"你真的分得清吗?
系统调用和库函数的区别


关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存